package credentialsprovider

import (
	"fmt"
	"strings"
	"time"

	"code.byted.org/bytertc/whiteboard_server/biz/config"
	"github.com/pkg/errors"

	"github.com/aws/aws-sdk-go/aws/credentials"

	"github.com/volcengine/volc-sdk-golang/service/sts"
)

type RoleType int

const (
	UnSpecified RoleType = iota
	UserOfVolc
	UserOfBytePlus
)

const (
	ProviderName            = "AssumeRoleProvider"
	MaxDurationOfAssumeRole = 12 * time.Hour

	//	TRN address：trn:iam:{{.region}}:{{.account}}:example_instance/{{.id}}
	//	TRN description：trn:serviceName:region:accountID:resourceName/resourceID

	// RtcRoleRtnFormat TRN：Resource global unique identifier
	RtcRoleRtnFormat = "trn:iam::%s:role/ServiceRoleForRTC"

	RtcRoleSessionNameFormat = "RtcAssume_%v"

	// InvalidParameter invalidAccountID
	InvalidParameter string = "\"Code\":\"InvalidParameter\""
	// RoleNotExist the accountID is not exist
	RoleNotExist string = "\"Code\":\"RoleNotExist\""
	// InvalidAccessKey invalid AK/SK
	InvalidAccessKey string = "InvalidAccessKey"
)

// A Provider is the interface for any component which will provide credentials
// Value. A provider is required to manage its own Expired state, and what to
// be expired means.
//
// The Provider should not need to implement its own mutexes, because
// that will be managed by Credentials.
type Provider interface {
	// Retrieve returns nil if it successfully retrieved the value.
	// Error is returned if the value were not obtainable, or empty.
	Retrieve() (credentials.Value, error)

	// IsExpired returns if the credentials are no longer valid, and need
	// to be retrieved.
	IsExpired() bool
}

type AssumeRoler interface {
	AssumeRole(req *sts.AssumeRoleRequest) (*sts.AssumeRoleResp, int, error)
}

type AssumeRoleProvider struct {
	credentials.Expiry
	assumeRoler     AssumeRoler
	roleType        RoleType
	roleTrn         string
	roleSessionName string
	duration        time.Duration
	expiryWindow    time.Duration
}

func WithAssumeRoler(ar AssumeRoler) func(*AssumeRoleProvider) {
	return func(arp *AssumeRoleProvider) {
		arp.assumeRoler = ar
	}
}

func NewAssumeRoleProvider(roleType RoleType, roleTrn string, options ...func(*AssumeRoleProvider)) (*AssumeRoleProvider, error) {
	arp := &AssumeRoleProvider{
		roleType:        roleType,
		roleTrn:         roleTrn,
		roleSessionName: fmt.Sprintf(RtcRoleSessionNameFormat, time.Now().UTC().UnixNano()),
		duration:        MaxDurationOfAssumeRole,
		expiryWindow:    MaxDurationOfAssumeRole / 2,
	}
	for _, op := range options {
		op(arp)
	}

	if arp.assumeRoler == nil {
		var err error
		if arp.assumeRoler, err = NewAssumeRoler(arp.roleType); err != nil {
			return nil, err
		}
	}
	return arp, nil
}

func (arp *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
	startTime := time.Now()
	stsCredentials, err := arp.AssumeRole()
	if err != nil {
		return credentials.Value{ProviderName: ProviderName}, err
	}
	// We will proactively generate new credentials before they expire.
	arp.SetExpiration(startTime.Add(arp.duration), arp.expiryWindow)
	return credentials.Value{
		AccessKeyID:     stsCredentials.AccessKeyId,
		SecretAccessKey: stsCredentials.SecretAccessKey,
		SessionToken:    stsCredentials.SessionToken,
		ProviderName:    ProviderName,
	}, nil
}

func (arp *AssumeRoleProvider) AssumeRole() (*sts.Credentials, error) {
	req := &sts.AssumeRoleRequest{
		// DurationSeconds [900s, 12h]
		DurationSeconds: int(arp.duration.Seconds()),
		RoleTrn:         arp.roleTrn,
		// Try to work out a role name that will hopefully end up unique.
		RoleSessionName: arp.roleSessionName,
	}
	// bad resp of AssumeRole
	resp, statusCode, err := arp.assumeRoler.AssumeRole(req)
	if err != nil {
		err = errors.Wrap(err, fmt.Sprintf("get credentials failed, status code: %v", statusCode))
		if strings.Contains(err.Error(), InvalidParameter) ||
			strings.Contains(err.Error(), RoleNotExist) {
			return nil, errors.WithStack(err)
		}
		return nil, err
	}
	if resp != nil && resp.ResponseMetadata.Error != nil {
		err2 := fmt.Errorf("get credentials failed, err: %#v", *resp.ResponseMetadata.Error)
		switch resp.ResponseMetadata.Error.Code {
		case InvalidAccessKey:
			return nil, errors.WithStack(err2)
		}
		return nil, err2
	}
	if resp.Result == nil || resp.Result.Credentials == nil {
		return nil, fmt.Errorf("invalid resp of getting credentials, resp: %#v", resp)
	}
	return resp.Result.Credentials, nil
}

func NewAssumeRoler(roleType RoleType) (*sts.STS, error) {
	var (
		host string
		ak   string
		sk   string
	)
	switch roleType {
	case UserOfVolc:
		host = config.GetConfig().StorageConfig.VolcApiHost
		ak = config.GetConfig().StorageConfig.VolcRtcAk
		sk = config.GetConfig().StorageConfig.VolcRtcSk

	case UserOfBytePlus:
		host = config.GetConfig().StorageConfig.BytePlusApiHost
		ak = config.GetConfig().StorageConfig.BytePlusRtcAk
		sk = config.GetConfig().StorageConfig.BytePlusRtcSk

	default:
		return nil, errors.WithStack(fmt.Errorf("unknown RoleType: %v", roleType))
	}
	if host == "" || ak == "" || sk == "" {
		return nil, errors.WithStack(fmt.Errorf("invalid env, roleType: %v, host: %v, ak: %v, sk: %v",
			roleType, host, ak, sk))
	}
	instance := sts.NewInstance()
	instance.Client.SetHost(host)
	instance.Client.SetAccessKey(ak)
	instance.Client.SetSecretKey(sk)
	return instance, nil
}
